OptionsBasedMetricRecordingFilter Class

Namespace: Diginsight.Diagnostics
Assembly: Diginsight.Diagnostics.dll

Configuration-based filter for controlling which activities record span duration metrics, using pattern matching rules defined in application settings.

public class OptionsBasedMetricRecordingFilter : IMetricRecordingFilter

Inheritance

Object ? OptionsBasedMetricRecordingFilter

Implements

  • IMetricRecordingFilter

Summary

The OptionsBasedMetricRecordingFilter class enables declarative filtering of metric recording through configuration files (e.g., appsettings.json). It uses pattern matching against activity source names and operation names to determine whether metrics should be recorded, providing fine-grained control over which operations generate telemetry data without code changes.

Key capabilities: - ? Configuration-driven filtering - control metric recording via appsettings.json - ? Pattern matching - use wildcards (*) and pipe separators (|) for flexible matching - ? Instrument-specific options - different rules per metric instrument - ? Hierarchical evaluation - instrument-specific rules override general rules - ? All-or-nothing logic - all matching patterns must agree to record - ? Virtualizable method - easily extensible for custom filtering logic


Constructors

OptionsBasedMetricRecordingFilter(IOptionsMonitor)

Initializes a new instance of the OptionsBasedMetricRecordingFilter class.

public OptionsBasedMetricRecordingFilter(
    IOptionsMonitor<OptionsBasedMetricRecordingFilterOptions> filterMonitor
)

Parameters

filterMonitor : IOptionsMonitor<OptionsBasedMetricRecordingFilterOptions>
Monitor for accessing filter configuration options. Uses IOptionsMonitor to support dynamic configuration changes at runtime without restarting the application.

Remarks

The constructor stores the options monitor which allows: - Access to named options for instrument-specific configurations - Hot reload support - configuration changes apply immediately - Default configuration via CurrentValue property


Methods

ShouldRecord(Activity, Instrument)

Determines whether a metric should be recorded for the specified activity and instrument.

public virtual bool? ShouldRecord(Activity activity, Instrument instrument)

Returns

bool?
- true - Record the metric (all matching patterns returned true) - false - Skip recording (no patterns matched, or matched patterns didn’t all agree) - null - Not applicable (defers to other filters or default configuration)

Parameters

activity : Activity
The activity being evaluated for metric recording.

instrument : Instrument
The metric instrument that would record the measurement (e.g., histogram for span duration).

Remarks

This method implements a two-tier filtering strategy:

  1. Instrument-specific filtering: First checks configuration named by instrument.Name
  2. General filtering: Falls back to default (unnamed) configuration if no specific rules match

Evaluation logic:

// Step 1: Try instrument-specific rules
var specificMatches = GetMatches(filterMonitor.Get(instrument.Name));
if (specificMatches.Any())
    return specificMatches.All(x => x);  // All must be true

// Step 2: Try general rules
var generalMatches = GetMatches(filterMonitor.CurrentValue);
return generalMatches.Any() && generalMatches.All(x => x);  // At least one, all must be true

Pattern matching: - Extracts activity.Source.Name and activity.OperationName - Compares against configured patterns using ActivityUtils.FullNameMatchesPattern - If multiple patterns match, all must return the same value (true or false)

Return value determination: - If any patterns match for instrument-specific config ? returns true only if all matched patterns are true - Otherwise, if any patterns match for general config ? returns true only if all matched patterns are true - If no patterns match ? returns false

Example

Configuration:

{
  "OptionsBasedMetricRecordingFilter": {
    "ActivityNames": {
      "MyApp.Orders.*": true,
      "MyApp.Database.*": true,
      "System.*": false
    }
  },
  "diginsight.span_duration": {
    "ActivityNames": {
      "MyApp.Orders.SlowOperation": false,
      "MyApp.*": true
    }
  }
}

Filter behavior:

// Activity: Source="MyApp.Orders", Operation="ProcessOrder"
// Instrument: Name="diginsight.span_duration"
// Result: true (matches "MyApp.*" in specific config)

// Activity: Source="MyApp.Orders", Operation="SlowOperation"  
// Instrument: Name="diginsight.span_duration"
// Result: false (matches specific pattern "MyApp.Orders.SlowOperation": false)

// Activity: Source="System.Net.Http", Operation="HttpRequest"
// Instrument: Name="diginsight.span_duration"
// Result: false (matches "System.*": false in general config)

// Activity: Source="ThirdParty.Library", Operation="DoWork"
// Instrument: Name="custom.metric"
// Result: false (no patterns match)

Pattern Syntax

Basic Patterns

Pattern matching is performed by ActivityUtils.FullNameMatchesPattern and supports:

Operation Name Only

Match by operation name alone:

{
  "ActivityNames": {
    "ProcessOrder": true,           // Exact match
    "Process*": true,                // Starts with "Process"
    "*Order": true,                  // Ends with "Order"
    "*Process*": true                // Contains "Process"
  }
}

Full Name Pattern (Source|Operation)

Match by source name AND operation name using pipe separator:

{
  "ActivityNames": {
    "MyApp.Orders|ProcessOrder": true,      // Exact source and operation
    "MyApp.*|Process*": true,               // Wildcard source and operation
    "MyApp.Orders|": true,                  // Any operation in MyApp.Orders
    "|ProcessOrder": true                   // ProcessOrder in any source
  }
}

Wildcard Rules

Single wildcard (*): - At the beginning: matches any prefix (*Order matches ProcessOrder, CreateOrder) - At the end: matches any suffix (Process* matches ProcessOrder, ProcessPayment) - Both sides: matches if the middle part is contained (*Process* matches GetProcessStatus) - Alone: matches everything (* matches all)

Multiple wildcards: - Not supported - pattern parsing throws ArgumentException - Use multiple pattern entries instead

Case sensitivity: - Pattern matching is case-insensitive

Pattern Precedence

When multiple patterns match the same activity:

  1. Instrument-specific patterns (named options) take precedence over general patterns
  2. All matching patterns must return the same value (true or false)
  3. If patterns conflict (some true, some false), the result is determined by the All() logic

Example of conflicting patterns:

{
  "ActivityNames": {
    "MyApp.Orders.*": true,
    "MyApp.Orders.SlowOperation": false
  }
}

For MyApp.Orders.SlowOperation: - Both patterns match - One returns true, one returns false - All(x => x) returns false (not all are true) - Result: false (don’t record)


Configuration

OptionsBasedMetricRecordingFilterOptions

Configure the filter in appsettings.json:

{
  "OptionsBasedMetricRecordingFilter": {
    "ActivityNames": {
      "<pattern>": true|false
    }
  }
}

Properties

ActivityNames : IDictionary<string, bool>
Dictionary mapping activity name patterns to recording decisions: - Key: Pattern string (supports wildcards and pipe separator) - Value: true to record metrics, false to skip

Named Configuration (Instrument-Specific)

Configure different rules for specific metric instruments:

{
  "OptionsBasedMetricRecordingFilter": {
    "ActivityNames": {
      "MyApp.*": true
    }
  },
  "diginsight.span_duration": {
    "ActivityNames": {
      "MyApp.Orders.*": true,
      "MyApp.Inventory.*": false
    }
  },
  "custom.metric.name": {
    "ActivityNames": {
      "MyApp.Critical.*": true
    }
  }
}

How it works: - Section name matches instrument.Name parameter - Instrument-specific rules evaluated first - Falls back to OptionsBasedMetricRecordingFilter section if no specific rules match


Usage Examples

Basic Registration

Register the filter during application startup:

var builder = WebApplication.CreateBuilder(args);

// Register filter
builder.Services.AddSingleton<IMetricRecordingFilter, OptionsBasedMetricRecordingFilter>();

// Register metric recorder (uses the filter)
builder.Services.AddSpanDurationMetricRecorder();

var app = builder.Build();
app.Run();

Simple Configuration

appsettings.json:

{
  "OptionsBasedMetricRecordingFilter": {
    "ActivityNames": {
      "MyApp.Orders.*": true,
      "MyApp.Payment.*": true,
      "MyApp.Inventory.CheckAvailability": true,
      "Microsoft.AspNetCore.*": false,
      "System.*": false
    }
  }
}

Result: - ? Records: MyApp.Orders.ProcessOrder, MyApp.Payment.Charge - ? Records: MyApp.Inventory.CheckAvailability - ? Skips: Microsoft.AspNetCore.Hosting.HttpRequestIn - ? Skips: System.Net.Http.HttpRequestOut

Advanced Pattern Matching

appsettings.json:

{
  "OptionsBasedMetricRecordingFilter": {
    "ActivityNames": {
      "MyApp.*|Process*": true,          // Any Process* operation in MyApp.*
      "MyApp.*|*Slow*": false,           // Skip operations containing "Slow"
      "ThirdParty.Library|ImportantOp": true,  // Specific operation in specific source
      "*|HealthCheck": false,            // HealthCheck in any source
      "MyApp.Background.*": false        // All background operations
    }
  }
}

Test cases:

// MyApp.Orders | ProcessOrder ? true (matches "MyApp.*|Process*")
// MyApp.Orders | ProcessSlowOrder ? false (matches "*|*Slow*")
// MyApp.Orders | CreateOrder ? false (no match)
// ThirdParty.Library | ImportantOp ? true (exact match)
// ThirdParty.Library | OtherOp ? false (no match)
// MyApp.Health | HealthCheck ? false (matches "*|HealthCheck")

Instrument-Specific Configuration

Different rules for different metrics:

appsettings.json:

{
  "OptionsBasedMetricRecordingFilter": {
    "ActivityNames": {
      "MyApp.*": true
    }
  },
  "diginsight.span_duration": {
    "ActivityNames": {
      "MyApp.Orders.SlowOperation": false,
      "MyApp.Orders.*": true
    }
  },
  "custom.request_count": {
    "ActivityNames": {
      "MyApp.*": true,
      "ThirdParty.*": true
    }
  }
}

Behavior:

// For span_duration instrument:
// MyApp.Orders.ProcessOrder ? true
// MyApp.Orders.SlowOperation ? false (specific exclusion)
// MyApp.Inventory.CheckStock ? false (not matched in specific config, no general match)

// For custom.request_count instrument:
// MyApp.Orders.ProcessOrder ? true
// ThirdParty.Library.Execute ? true

// For other instruments:
// MyApp.Orders.ProcessOrder ? true (general config)

Multiple Filters

Combine with other filters for complex scenarios:

builder.Services.AddSingleton<IMetricRecordingFilter, OptionsBasedMetricRecordingFilter>();
builder.Services.AddSingleton<IMetricRecordingFilter, HttpHeadersSpanDurationMetricRecordingFilter>();
builder.Services.AddSingleton<IMetricRecordingFilter, CustomBusinessLogicFilter>();

Filter execution order: - All registered filters are evaluated - If any filter returns non-null, that result takes precedence - SpanDurationMetricRecorder uses the filter result with ?? fallback to configuration

Hot Reload Configuration

Changes to appsettings.json apply immediately without restart:

// Before (recording Orders)
{
  "OptionsBasedMetricRecordingFilter": {
    "ActivityNames": {
      "MyApp.Orders.*": true
    }
  }
}

// After hot reload (no longer recording Orders)
{
  "OptionsBasedMetricRecordingFilter": {
    "ActivityNames": {
      "MyApp.Orders.*": false,
      "MyApp.Payment.*": true
    }
  }
}

Implementation: - Uses IOptionsMonitor<T> for reactive configuration - No application restart required - Changes apply to next activity evaluation

Custom Derived Filter

Extend for custom logic:

public class BusinessRulesMetricFilter : OptionsBasedMetricRecordingFilter
{
    private readonly IFeatureFlagService _featureFlags;
    
    public BusinessRulesMetricFilter(
        IOptionsMonitor<OptionsBasedMetricRecordingFilterOptions> filterMonitor,
        IFeatureFlagService featureFlags)
        : base(filterMonitor)
    {
        _featureFlags = featureFlags;
    }
    
    public override bool? ShouldRecord(Activity activity, Instrument instrument)
    {
        // Check feature flag first
        if (!_featureFlags.IsEnabled("DetailedMetrics"))
            return false;
        
        // Fallback to configuration-based filtering
        return base.ShouldRecord(activity, instrument);
    }
}

Registration:

builder.Services.AddSingleton<IMetricRecordingFilter, BusinessRulesMetricFilter>();

Performance Considerations

Pattern Matching Performance

Efficiency: - Pattern matching uses string.StartsWith() and string.EndsWith() - Case-insensitive comparison has minimal overhead - Frozen options prevent modification during evaluation

Optimization tips: - ? Use fewer, broader patterns over many specific patterns - ? Place most common patterns first in configuration - ? Avoid excessive wildcard patterns that match everything

Memory Efficiency

Configuration freezing:

((IOptionsBasedMetricRecordingFilterOptions)options.Freeze())
  • Options are frozen to immutable collections before evaluation
  • Prevents modification during concurrent access
  • Creates immutable dictionary copy only once per instrument

No allocations during filtering: - LINQ operations use ToArray() to avoid multiple enumerations - Pattern matching uses stack-allocated spans where possible

Early Exit Optimization

Instrument-specific rules:

IEnumerable<bool> specificMatches = GetMatches(filterMonitor.Get(instrument.Name));
if (specificMatches.Any())
    return specificMatches.All(static x => x);
  • If instrument-specific rules match, general rules are never evaluated
  • Reduces configuration lookups by ~50% for named instruments

Thread Safety

The OptionsBasedMetricRecordingFilter is thread-safe:

  • ? IOptionsMonitor<T> is thread-safe by design
  • ? Freeze() creates immutable snapshot for evaluation
  • ? No mutable state between filter evaluations
  • ? Pattern matching logic is stateless

Multiple threads can evaluate different activities concurrently without synchronization issues.


Troubleshooting

Metrics Not Being Filtered

Symptoms: All activities record metrics despite filter configuration.

Checklist: 1. ? Is the filter registered in DI container? csharp builder.Services.AddSingleton<IMetricRecordingFilter, OptionsBasedMetricRecordingFilter>();

  1. ? Is the configuration section correctly named?

    "OptionsBasedMetricRecordingFilter": { ... }
  2. ? Are patterns matching the expected activities?

    // Debug: check activity properties
    Console.WriteLine($"Source: {activity.Source.Name}");
    Console.WriteLine($"Operation: {activity.OperationName}");
  3. ? Are multiple filters registered? Check precedence order.

Pattern Not Matching

Symptoms: Expected pattern doesn’t match activities.

Common issues:

? Wrong separator:

// Wrong - using colon
"MyApp.Orders:ProcessOrder": true

// Correct - using pipe
"MyApp.Orders|ProcessOrder": true

? Case mismatch assumptions:

// Unnecessary - matching is case-insensitive
"myapp.orders.*": true  // Works the same as "MyApp.Orders.*"

? Multiple wildcards:

// Invalid - throws ArgumentException
"MyApp.*.Orders.*": true

// Valid - single wildcard
"MyApp.*": true

Debugging tool:

var result = ActivityUtils.FullNameMatchesPattern(
    "MyApp.Orders",           // source name
    "ProcessOrder",           // operation name
    "MyApp.Orders|Process*"   // pattern
);
// Returns: true

Unexpected Filter Results

Symptoms: Activities recording/not recording contrary to expectations.

Cause 1: Multiple patterns conflict

{
  "ActivityNames": {
    "MyApp.*": true,
    "MyApp.Orders.SlowOp": false
  }
}

For MyApp.Orders.SlowOp: - Both patterns match - All(x => x) evaluates to false (not all are true) - Result: false (won’t record)

Solution: Be explicit with more specific patterns, or use exclusions only.

Cause 2: Instrument-specific overrides general

{
  "OptionsBasedMetricRecordingFilter": {
    "ActivityNames": { "MyApp.*": true }
  },
  "diginsight.span_duration": {
    "ActivityNames": { "MyApp.Orders.*": false }
  }
}

For MyApp.Orders.ProcessOrder with diginsight.span_duration instrument: - Checks instrument-specific config first - Matches "MyApp.Orders.*": false - Result: false (never checks general config)

Solution: Understand the two-tier evaluation strategy.

Performance Impact

Symptoms: High CPU usage or latency when recording metrics.

Diagnosis:

// Check how many patterns are evaluated
var options = serviceProvider.GetService<IOptions<OptionsBasedMetricRecordingFilterOptions>>();
Console.WriteLine($"Pattern count: {options.Value.ActivityNames.Count}");

Mitigation: - Reduce number of configured patterns - Use broader patterns (e.g., MyApp.* instead of listing each namespace) - Consider custom filter with caching for complex scenarios


Design Patterns

Strategy Pattern

The filter implements the Strategy pattern: - IMetricRecordingFilter defines the strategy interface - OptionsBasedMetricRecordingFilter is one concrete strategy - SpanDurationMetricRecorder uses the strategy without knowing implementation details

Options Pattern

Uses the .NET Options pattern: - IOptionsMonitor<T> for reactive configuration - Named options for instrument-specific configuration - Hot reload support without restart

Template Method Pattern

Virtual ShouldRecord method enables customization:

public class CustomFilter : OptionsBasedMetricRecordingFilter
{
    public override bool? ShouldRecord(Activity activity, Instrument instrument)
    {
        // Custom pre-processing
        // ...
        
        // Call base implementation
        return base.ShouldRecord(activity, instrument);
    }
}

Best Practices

Configuration Organization

? DO group related patterns:

{
  "ActivityNames": {
    "MyApp.Orders.*": true,
    "MyApp.Payment.*": true,
    "MyApp.Shipping.*": true,
    "System.*": false,
    "Microsoft.*": false
  }
}

? DO use instrument-specific sections for special cases:

{
  "OptionsBasedMetricRecordingFilter": {
    "ActivityNames": { "MyApp.*": true }
  },
  "expensive.custom.metric": {
    "ActivityNames": { "MyApp.Critical.*": true }
  }
}

? DON’T create overly complex pattern hierarchies:

{
  "ActivityNames": {
    "MyApp.*": true,
    "MyApp.Orders.*": false,
    "MyApp.Orders.Critical.*": true,
    "MyApp.Orders.Critical.Slow*": false
  }
}

Pattern Design

? DO start with broad patterns and add exceptions:

{
  "ActivityNames": {
    "MyApp.*": true,
    "MyApp.Internal.*": false
  }
}

? DO explicitly exclude noisy framework activities:

{
  "ActivityNames": {
    "Microsoft.AspNetCore.*": false,
    "System.Net.Http.*": false
  }
}

? DON’T mix inclusion and exclusion without clear precedence:

{
  "ActivityNames": {
    "MyApp.*": true,
    "MyApp.Orders.*": false,
    "MyApp.Orders.Special": true  // Confusing!
  }
}

Testing Filters

[Fact]
public void ShouldRecord_MatchesPattern_ReturnsTrue()
{
    // Arrange
    var options = new OptionsBasedMetricRecordingFilterOptions
    {
        ActivityNames = { ["MyApp.Orders.*"] = true }
    };
    var monitor = Mock.Of<IOptionsMonitor<OptionsBasedMetricRecordingFilterOptions>>(
        m => m.CurrentValue == options && m.Get(It.IsAny<string>()) == new OptionsBasedMetricRecordingFilterOptions());
    
    var filter = new OptionsBasedMetricRecordingFilter(monitor);
    
    var activitySource = new ActivitySource("MyApp.Orders");
    using var activity = activitySource.StartActivity("ProcessOrder");
    var instrument = Mock.Of<Instrument>(i => i.Name == "test.metric");
    
    // Act
    var result = filter.ShouldRecord(activity, instrument);
    
    // Assert
    Assert.True(result);
}

Version History

Version Changes
3.0.0 Initial release with IMetricRecordingFilter support
3.1.0 Added instrument-specific named configuration support
3.2.0 Improved pattern matching performance with frozen options

See Also


Remarks

The OptionsBasedMetricRecordingFilter provides a declarative, configuration-driven approach to metric filtering that enables operations teams to control telemetry costs without code changes. By leveraging pattern matching and hierarchical configuration, it offers both simplicity for common scenarios and flexibility for complex filtering requirements.

Design principles: - ?? Configuration over code - change behavior via settings, not deployments - ? Performance-optimized - minimal overhead with frozen options and early exit - ?? Flexible pattern matching - wildcards and source|operation syntax - ?? Instrument-aware - different rules for different metrics - ?? Hot reload - configuration changes apply immediately - ?? Extensible - virtual method enables custom subclasses

Common use cases: - Filter out noisy framework activities to reduce costs - Record metrics only for business-critical operations - Different sampling rates per metric type - Environment-specific filtering (dev vs. production) - Temporary metric collection for troubleshooting

Integration:

builder.Services
    .AddSingleton<IMetricRecordingFilter, OptionsBasedMetricRecordingFilter>()
    .AddSpanDurationMetricRecorder();

This configuration-based approach aligns with the “Infrastructure as Configuration” principle, making telemetry behavior observable and version-controlled through application settings.

Back to top